Skip to content

feat(signal): add typed legacy session interop - #1072

Merged
jlucaso1 merged 5 commits into
mainfrom
agent/legacy-session-v1-interop
Jul 23, 2026
Merged

feat(signal): add typed legacy session interop#1072
jlucaso1 merged 5 commits into
mainfrom
agent/legacy-session-v1-interop

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add a typed, transport-agnostic model for decoded legacy libsignal SessionRecord v1 data, behind the new legacy-session-interop cargo feature (default off, forwarded through wacore and the root crate)
  • centralize sender/receiver roles, counter conversion, lifecycle ordering, pruning, ratchet reconstruction, and skipped-message key derivation in the Signal core
  • provide faithful import into canonical SessionRecord and an explicit operational reverse projection
  • preserve skipped keys on closed and open receiver chains across archived sessions, and let the canonical decrypt path consume them

Design

The new boundary accepts owned binary values and moves them into the canonical component model. It reuses the existing key validators, SessionRecord::from_components, SessionMessageKeyMaterial::Seed, message-key derivation, and canonical record limits. It does not add textual serialization, transport-specific DTOs, dependencies, caches, schemas, or shadow state. The interop model compiles out of native builds; only migration consumers enable the feature.

The reverse projection is intentionally explicit about information that the legacy format cannot represent exactly. Derived skipped-message keys, unsupported session state, ambiguous remote-ratchet state, and a pending pre-key base that differs from the session base key return typed deterministic errors instead of being discarded or inferred silently. Existing sender-key components remain the single model for sender-key records.

Decrypt-path behavior

These changes stay outside the feature gate because canonical records reach the same states natively (re-initiations reuse the peer's signed pre-key as a ratchet key, so sessions can legitimately share receiver ratchet keys):

  • a skipped key missing from a recognized chain, open or closed, is a non-terminal candidate failure: the archived-session search continues, so a delayed message whose key survives only in an archived session still decrypts
  • the final classification prefers a recognized duplicate over candidate-session BadMac noise, so a replay is acknowledged as a duplicate instead of triggering a retry receipt for an already-processed message

Compatibility and safety

  • validates outer session indexes against base keys and rejects duplicate or multiple-current sessions
  • validates chain roles, key lengths, counters, skipped-key indexes, pending pre-keys, and canonical limits before reconstruction
  • imports previousCounter of -1 (a ratchet step over a sending chain that never sent, the reference seed value) as zero; other out-of-range values fail typed
  • keeps receiver-chain chronology and canonical archived-session pruning behavior
  • redacts cryptographic material from Debug output
  • preserves canonical session serialization and Signal persistence semantics

The reverse conversion is an operational projection, not a byte-exact round trip: legacy lifecycle timestamps and base-key lookup roles are reconstructed deterministically, while genuinely non-representable state is rejected.

Performance

  • moves owned byte buffers through Bytes and sorts collections in place
  • adds no allocation to the existing decrypt hot path; the internal decrypt error wrapper was removed outright
  • default builds compile the interop model out; the earlier ungated release A/B measured +896 bytes (about 0.009%), now carried only by consumers enabling the feature
  • no change to .rodata, .data, or static state

Validation

  • cargo fmt --all -- --check
  • cargo clippy -p wacore-libsignal -p wacore-derive --all-targets -- -D warnings, with and without legacy-session-interop
  • cargo test -p wacore-libsignal, with and without legacy-session-interop
  • cargo test --workspace --exclude e2e-tests
  • full workspace all-features lint and test runs in CI

Tests cover reference-shaped current and archived sessions, chain ordering and limits, checked counter conversion including the -1 floor, pending pre-keys, malformed and duplicate state, exact skipped-seed derivation, explicit reverse-projection failures including the pending-base mismatch, sender-key component round trips, serialization/reload of rebuilt records, real decryption using skipped keys from closed and open archived chains, and duplicate classification winning over candidate MAC failures.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d91f8a85-5f84-468b-8ce4-664cc3d27107

📥 Commits

Reviewing files that changed from the base of the PR and between dee3426 and 523a52b.

📒 Files selected for processing (1)
  • wacore/libsignal/src/protocol/session_cipher.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added optional interoperability for importing and exporting legacy Signal session records.
    • Added validation and safe handling for legacy session state, including redacted diagnostic output.
    • Added stricter numeric enum decoding with clear errors for unknown values when no fallback is available.
  • Bug Fixes

    • Improved message decryption when receiver-chain keys are archived or unavailable.
    • Duplicate messages are now consistently classified as replays instead of triggering misleading retry or authentication failures.

Walkthrough

Adds public legacy Signal SessionRecord V1 interoperability types and bidirectional conversion with canonical sessions. It also models closed receiver chains explicitly so persisted skipped-message keys remain decryptable after live chain-key bytes are absent.

Changes

Legacy session interoperability

Layer / File(s) Summary
V1 contracts and public API
wacore/libsignal/src/protocol/legacy_session.rs, wacore/libsignal/src/protocol/mod.rs, wacore/derive/src/lib.rs, wacore/libsignal/Cargo.toml, wacore/Cargo.toml, Cargo.toml
Defines V1 session models, semantic enums, counter translation, typed errors, redacted debug output, feature wiring, strict integer enum decoding, and public protocol re-exports.
V1 import and validation
wacore/libsignal/src/protocol/legacy_session.rs
Validates indexed sessions, key material, pending pre-keys, ratchet chains, skipped-message keys, and ordering before constructing canonical session state.
Canonical projection and interoperability tests
wacore/libsignal/src/protocol/legacy_session.rs
Projects canonical sessions into operational V1 records, rejects unsupported fields, and covers conversion, ordering, derivation, wire reload, and redaction behavior.

Closed receiver-chain decryption

Layer / File(s) Summary
Receiver-chain state model
wacore/libsignal/src/protocol/state/session.rs, wacore/libsignal/src/protocol/state/mod.rs
Introduces ReceiverChainState::Open and Closed, deriving the state from stored receiver-chain key material.
Closed-chain decrypt flow
wacore/libsignal/src/protocol/session_cipher.rs
Propagates receiver-chain state through decryption, derives keys for closed chains, and rejects attempts to create keys for closed chains.
Persisted skipped-key regression coverage
wacore/libsignal/src/protocol/session_cipher.rs
Verifies closed and open receiver-chain archive searches and ensures recognized duplicates take precedence over candidate-session MAC failures.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LegacySessionRecordV1
  participant V1Validation
  participant SessionRecord
  LegacySessionRecordV1->>V1Validation: validate indexed sessions and chains
  V1Validation->>SessionRecord: build canonical SessionComponents
  SessionRecord-->>LegacySessionRecordV1: return converted session record
Loading
sequenceDiagram
  participant SessionCipher
  participant SessionState
  participant MessageKeyDerivation
  SessionCipher->>SessionState: receiver_chain_state(sender)
  SessionState-->>SessionCipher: Open or Closed(next_index)
  SessionCipher->>MessageKeyDerivation: get_message_keys(counter)
  MessageKeyDerivation-->>SessionCipher: persisted skipped-message key
Loading

Possibly related PRs

Suggested labels: api-design

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: typed legacy session interop for Signal.
Description check ✅ Passed The description accurately describes the new legacy session interop feature and its decrypt-path behavior.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/legacy-session-v1-interop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds typed interoperability with legacy libsignal session records. The main changes are:

  • Feature-gated legacy session import and operational projection.
  • Validation and reconstruction of ratchet chains, counters, pending pre-keys, and skipped keys.
  • Archived-session decryption search and duplicate classification updates.
  • Strict numeric WireEnum conversion for enums without fallback variants.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues were found in the updated code.
  • The feature remains disabled by default and its exports follow the feature gate.
  • The latest fixes preserve the validated session conversion and decryption behavior.

Important Files Changed

Filename Overview
wacore/libsignal/src/protocol/legacy_session.rs Adds the typed legacy model, validation, canonical import, operational projection, ordering, pruning, and conversion tests.
wacore/libsignal/src/protocol/session_cipher.rs Continues archived-session search when a recognized chain lacks a skipped key and gives duplicate classification precedence over candidate MAC failures.
wacore/libsignal/src/protocol/state/session.rs Extends canonical receiver-chain state to preserve and consume imported skipped-message keys.
wacore/derive/src/lib.rs Generates strict numeric conversion and deserialization for integer wire enums without fallback variants.
wacore/libsignal/Cargo.toml Adds the opt-in interoperability feature and derive dependency.
Cargo.toml Forwards the interoperability feature through the root crate.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Decoded legacy session] --> B[Validate legacy state]
    B --> C[Order and prune sessions]
    C --> D[Build canonical SessionRecord]
    D --> E[Canonical decrypt search]
    D --> F[Operational legacy projection]
Loading

Reviews (4): Last reviewed commit: "fix(signal): log terminal duplicate clas..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 9.68 MiB 9.68 MiB +1.75 KiB (+0.02%) 🔺
bin .text 7.74 MiB 7.74 MiB +1.75 KiB (+0.02%) 🔺
bin allocated (text+data+bss) 9.68 MiB 9.68 MiB +32 B (+0.00%) 🔺
llvm-lines wacore 493,286 493,286 0
llvm-lines wacore copies 16,341 16,341 0
llvm-lines whatsapp-rust lib 688,920 689,294 +374 (+0.05%) 🔺
llvm-lines whatsapp-rust lib copies 21,955 21,958 +3 (+0.01%) 🔺
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.68 MiB 1.68 MiB +1.27 KiB (+0.07%) 🔺
.text wacore 638.98 KiB 638.98 KiB 0
.text wacore_binary 89.35 KiB 89.48 KiB +139 B (+0.15%) 🔺
.text wacore_libsignal 160.96 KiB 161.46 KiB +508 B (+0.31%) 🔺
.text wacore_appstate 22.36 KiB 22.36 KiB 0
.text wacore_noise 22.98 KiB 22.98 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 510.67 KiB 510.67 KiB 0
.text whatsapp_rust_tokio_transport 39.84 KiB 39.84 KiB 0
.text whatsapp_rust_ureq_http_client 10.28 KiB 10.28 KiB 0
.text std 968.47 KiB 968.47 KiB 0
.text other deps 1.88 MiB 1.88 MiB -143 B (-0.01%) 🔽
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.68 MiB 1.68 MiB +1.27 KiB (+0.07%)

Baseline: 2ab4bc0f4 (latest main run) · Head: 6526bf39c · Graphs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59d451c6e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/libsignal/src/protocol/legacy_session.rs Outdated
Comment thread wacore/libsignal/src/protocol/session_cipher.rs Outdated
Comment thread wacore/libsignal/src/protocol/legacy_session.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/libsignal/src/protocol/legacy_session.rs`:
- Around line 2044-2049: Update debug_output_never_contains_key_material to
assert that the output does not contain a repeated byte value present in
reference_session(80)'s key material, such as 84, instead of 80. Keep the
existing <redacted> assertion and ensure the negative check would fail if key
material were emitted by Debug.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0dda9eae-5a7b-4ded-8a34-0cb5f9b3eb6a

📥 Commits

Reviewing files that changed from the base of the PR and between 013a328 and 59d451c.

📒 Files selected for processing (5)
  • wacore/libsignal/src/protocol/legacy_session.rs
  • wacore/libsignal/src/protocol/mod.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/libsignal/src/protocol/state/mod.rs
  • wacore/libsignal/src/protocol/state/session.rs

Comment thread wacore/libsignal/src/protocol/legacy_session.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 053e76bcff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/libsignal/src/protocol/session_cipher.rs Outdated
Comment thread wacore/libsignal/src/protocol/legacy_session.rs
Comment thread wacore/libsignal/src/protocol/session_cipher.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
wacore/libsignal/src/protocol/session_cipher.rs (1)

2130-2133: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Round-trip the record through persistence in this regression.

This rebuilds an in-memory SessionRecord, but never serializes and reloads it. Add a serialization/reload round-trip before decrypting first to verify that the archived skipped key survives actual persisted state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/libsignal/src/protocol/session_cipher.rs` around lines 2130 - 2133,
Update the regression setup around bob_sessions and
SessionRecord::from_components to serialize the rebuilt record through the
existing persistence mechanism, reload it, and use the reloaded record before
decrypting first. Preserve the test’s existing session address and decryption
flow while ensuring the archived skipped key is validated from persisted state.
wacore/libsignal/src/protocol/legacy_session.rs (1)

415-416: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This error text lies about what actually went wrong. Fix it before it ships.

InvalidSignedPreKeyId says "is out of range," but per the library-context notes, out-of-range validation for the signed pre-key id was removed. The only remaining trigger (line 1137-1140) is a missing signed_pre_key_id (None), not an out-of-range value. Ship a message that actually describes the failure — I don't want engineers chasing ghosts in the logs.

🩹 Proposed fix
-    #[error("legacy session {session} signed pre-key id is out of range")]
+    #[error("legacy session {session} is missing a signed pre-key id")]
     InvalidSignedPreKeyId { session: usize },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/libsignal/src/protocol/legacy_session.rs` around lines 415 - 416,
Update the error message for InvalidSignedPreKeyId to describe a missing signed
pre-key id rather than an out-of-range value. Keep the error variant and its
session field unchanged, and align the text with the None-triggered failure at
the existing call site.
wacore/derive/src/lib.rs (1)

1117-1226: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Make ConnectFailureReason explicit about missing fallback resilience

VideoState and TempBanReason still have #[wire_fallback], which is appropriate. ConnectFailureReason omits it, but that may intentionally hard-fail on unknown server codes before logging out. Make that contract explicit in the enum docs so a future refactoring does not accidentally re-add forward-compatibility where strict failure was meant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/derive/src/lib.rs` around lines 1117 - 1226, Update the documentation
for ConnectFailureReason to explicitly state that it intentionally omits
#[wire_fallback] and strictly rejects unknown server codes. Preserve the
existing strict conversion behavior and do not add fallback handling; clarify
that this hard failure is intentional before logout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/derive/src/lib.rs`:
- Around line 1188-1199: Remove the unwrap from the strict_from_arms
construction by matching directly on i.wire.as_ref() and handling the expected
VariantWire::Int case without re-destructuring. Preserve the existing literal
generation and Ok arm output for valid integer variants, while using the
established non-panicking handling for any unexpected wire variant.

In `@wacore/libsignal/src/protocol/session_cipher.rs`:
- Around line 1272-1287: Update StateDecryptError to derive thiserror::Error,
annotate Protocol for transparent source conversion from SignalProtocolError,
and add the corresponding transparent conversion for InvalidSessionError if
supported by the enum design. Remove the manual From<SignalProtocolError> and
From<InvalidSessionError> implementations while preserving the existing
MissingClosedChainMessageKey variant.

---

Outside diff comments:
In `@wacore/derive/src/lib.rs`:
- Around line 1117-1226: Update the documentation for ConnectFailureReason to
explicitly state that it intentionally omits #[wire_fallback] and strictly
rejects unknown server codes. Preserve the existing strict conversion behavior
and do not add fallback handling; clarify that this hard failure is intentional
before logout.

In `@wacore/libsignal/src/protocol/legacy_session.rs`:
- Around line 415-416: Update the error message for InvalidSignedPreKeyId to
describe a missing signed pre-key id rather than an out-of-range value. Keep the
error variant and its session field unchanged, and align the text with the
None-triggered failure at the existing call site.

In `@wacore/libsignal/src/protocol/session_cipher.rs`:
- Around line 2130-2133: Update the regression setup around bob_sessions and
SessionRecord::from_components to serialize the rebuilt record through the
existing persistence mechanism, reload it, and use the reloaded record before
decrypting first. Preserve the test’s existing session address and decryption
flow while ensuring the archived skipped key is validated from persisted state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ba762144-8634-4ce3-ab88-89a5bf47cab6

📥 Commits

Reviewing files that changed from the base of the PR and between 59d451c and 053e76b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • wacore/derive/src/lib.rs
  • wacore/libsignal/Cargo.toml
  • wacore/libsignal/src/protocol/legacy_session.rs
  • wacore/libsignal/src/protocol/session_cipher.rs

Comment thread wacore/derive/src/lib.rs
Comment thread wacore/libsignal/src/protocol/session_cipher.rs Outdated
jlucaso1 added 2 commits July 23, 2026 09:40
A skipped key missing from a recognized open chain now falls through to
the archived-session search, matching the closed-chain path, and the
final classification prefers a recognized duplicate over candidate
BadMac noise so replays cannot trigger retry receipts. The internal
StateDecryptError wrapper is gone; both cases reuse DuplicatedMessage.

The reverse projection rejects a pending pre-key base that differs from
the session base key instead of emitting v1 state its own importer
refuses, and previousCounter -1 from a never-used sending chain imports
as zero while other out-of-range values fail typed.

Also renames MissingSignedPreKeyId to describe its only trigger, drops
unwraps from the derive int-mode arms, and reloads the rebuilt record
through the wire format in the skipped-key regression tests.
The legacy-session-interop feature (default off, forwarded through
wacore and the root crate) compiles the SessionRecord v1 typed model
and its conversions out of native builds, so only migration consumers
carry the interop surface. The session-search behavior stays ungated
because canonical records reach the same states.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
wacore/libsignal/src/protocol/session_cipher.rs (1)

1361-1392: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Closed branch logic is sound — but where's the test for the counter >= next_index rejection?

The bounds check at Lines 1373-1377 is a brand-new failure mode (InvalidSessionStructure("receiver chain is closed")) for a message that's genuinely beyond recovery once the chain key bytes are gone. None of the three new regression tests exercise it — they all stay under next_index. We don't ship security-critical ratchet code without a test for every new branch; add a case where counter is at or past next_index on a closed chain and assert the error.

🧪 Sketch of the missing test
#[test]
fn closed_receiver_chain_rejects_a_counter_at_or_past_next_index() {
    let (mut tp, first, mut components) = setup_skipped_key_scenario();
    let mut rng = rand::make_rng::<rand::rngs::StdRng>();

    let current = components.current_session.as_mut().expect("current");
    let receiver = receiver_chain_for(current, &first);
    receiver.chain_key.as_mut().expect("chain key").key = None;
    // Leave next_index as-is, but drop the skipped key for `first` so the
    // only remaining candidate for a counter >= next_index is rejected.
    receiver.message_keys.retain(|k| k.index != Some(first.counter()));
    install_record(&mut tp, components);

    futures::executor::block_on(async {
        let err = message_decrypt_signal(
            &first,
            &tp.alice_addr,
            &mut tp.bob_sessions,
            &mut tp.bob_identity,
            &mut rng,
        )
        .await
        .expect_err("counter beyond a closed chain's next_index must fail");
        assert!(matches!(err, SignalProtocolError::InvalidMessage(_, _)));
    });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/libsignal/src/protocol/session_cipher.rs` around lines 1361 - 1392,
Add a regression test alongside the existing session-cipher tests for
`decrypt_with_pending_state`’s closed receiver-chain branch, using the
skipped-key setup to clear the chain key, remove the relevant message key, and
submit a counter equal to or greater than `next_index`; assert decryption fails
with the expected invalid-message-wrapped error while preserving the existing
tests.
wacore/derive/src/lib.rs (1)

1162-1199: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Good, the .unwrap() from the last round is gone. Now let's stop writing the same match arm three times.

code_arms, from_arms, and strict_from_arms each re-derive (id, lit) from i.wire.as_ref() with an identical let Some(VariantWire::Int(n)) = ... else { unreachable!() } guard — the only difference is the trailing quote!. Every time a field is added to this shape, three near-identical closures need to change in lockstep. Pull the (id, lit) pair once and map each output from it.

♻️ Proposed consolidation
-    let code_arms: Vec<_> = infos
-        .iter()
-        .filter(|i| !i.is_fallback)
-        .map(|i| {
-            let id = &i.ident;
-            let Some(VariantWire::Int(n)) = i.wire.as_ref() else {
-                unreachable!()
-            };
-            let lit = proc_macro2::Literal::i32_suffixed(*n);
-            quote! { `#name`::`#id` => `#lit` }
-        })
-        .collect();
-
-    let from_arms: Vec<_> = infos
-        .iter()
-        .filter(|i| !i.is_fallback)
-        .map(|i| {
-            let id = &i.ident;
-            let Some(VariantWire::Int(n)) = i.wire.as_ref() else {
-                unreachable!()
-            };
-            let lit = proc_macro2::Literal::i32_suffixed(*n);
-            quote! { `#lit` => `#name`::`#id` }
-        })
-        .collect();
-
-    let strict_from_arms: Vec<_> = infos
-        .iter()
-        .filter(|i| !i.is_fallback)
-        .map(|i| {
-            let id = &i.ident;
-            let Some(VariantWire::Int(n)) = i.wire.as_ref() else {
-                unreachable!()
-            };
-            let lit = proc_macro2::Literal::i32_suffixed(*n);
-            quote! { `#lit` => ::core::result::Result::Ok(`#name`::`#id`) }
-        })
-        .collect();
+    let known: Vec<_> = infos
+        .iter()
+        .filter(|i| !i.is_fallback)
+        .map(|i| {
+            let Some(VariantWire::Int(n)) = i.wire.as_ref() else {
+                unreachable!()
+            };
+            (&i.ident, proc_macro2::Literal::i32_suffixed(*n))
+        })
+        .collect();
+    let code_arms: Vec<_> = known.iter().map(|(id, lit)| quote! { `#name`::`#id` => `#lit` }).collect();
+    let from_arms: Vec<_> = known.iter().map(|(id, lit)| quote! { `#lit` => `#name`::`#id` }).collect();
+    let strict_from_arms: Vec<_> = known
+        .iter()
+        .map(|(id, lit)| quote! { `#lit` => ::core::result::Result::Ok(`#name`::`#id`) })
+        .collect();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/derive/src/lib.rs` around lines 1162 - 1199, Consolidate the repeated
`(id, lit)` extraction shared by `code_arms`, `from_arms`, and
`strict_from_arms` by deriving the non-fallback variant data once, including the
existing `VariantWire::Int` validation, then generate each arm collection from
that shared representation. Preserve the three distinct match-arm outputs and
the current unreachable behavior for invalid wire values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/libsignal/src/protocol/session_cipher.rs`:
- Around line 1046-1060: Update the terminal non-Whisper DuplicatedMessage
branch in the session decryption match to call log_decryption_failure with the
ciphertext, current_state, and duplicate error before returning. Preserve the
existing session-state update and returned error behavior.
- Around line 1155-1167: Update the non-Whisper DuplicatedMessage branch in the
session decryption flow to log the decryption failure via log_decryption_failure
before restoring the session and returning the error. Keep the existing
restore_previous_session and error propagation behavior unchanged, matching the
logging behavior of the adjacent Whisper branch.

---

Outside diff comments:
In `@wacore/derive/src/lib.rs`:
- Around line 1162-1199: Consolidate the repeated `(id, lit)` extraction shared
by `code_arms`, `from_arms`, and `strict_from_arms` by deriving the non-fallback
variant data once, including the existing `VariantWire::Int` validation, then
generate each arm collection from that shared representation. Preserve the three
distinct match-arm outputs and the current unreachable behavior for invalid wire
values.

In `@wacore/libsignal/src/protocol/session_cipher.rs`:
- Around line 1361-1392: Add a regression test alongside the existing
session-cipher tests for `decrypt_with_pending_state`’s closed receiver-chain
branch, using the skipped-key setup to clear the chain key, remove the relevant
message key, and submit a counter equal to or greater than `next_index`; assert
decryption fails with the expected invalid-message-wrapped error while
preserving the existing tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70f7685f-b376-4e13-99d5-3d60a3afec2e

📥 Commits

Reviewing files that changed from the base of the PR and between 053e76b and dee3426.

📒 Files selected for processing (7)
  • Cargo.toml
  • wacore/Cargo.toml
  • wacore/derive/src/lib.rs
  • wacore/libsignal/Cargo.toml
  • wacore/libsignal/src/protocol/legacy_session.rs
  • wacore/libsignal/src/protocol/mod.rs
  • wacore/libsignal/src/protocol/session_cipher.rs

Comment thread wacore/libsignal/src/protocol/session_cipher.rs
Comment thread wacore/libsignal/src/protocol/session_cipher.rs
The non-Whisper duplicate arms returned without a trace while the
adjacent Whisper arms log before continuing the search; a silently
terminated pre-key replay left no audit trail.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jlucaso1
jlucaso1 merged commit 6a619d8 into main Jul 23, 2026
25 checks passed
@jlucaso1
jlucaso1 deleted the agent/legacy-session-v1-interop branch July 23, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant